﻿using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Homework3
{
    public class Lore
    {
        /// <summary>
        /// The types of operations which can be performed
        /// </summary>
        public enum OperatorType
        {
            Project = 0,
            Join,
            Scan,
            Aggr,
            Select
        };

        /// <summary>
        /// Stores an operation and its arguments for use in the operation stack
        /// </summary>
        public class OperatorObject
        {
            public OperatorType Type;
            public List<object> Arguments;
        }

        /// <summary>
        /// Stores the originating parent slot, target label, and current value for an OA slot
        /// </summary>
        private class OaSlotObject
        {
            public int ParentSlot = -1;
            public string ObjectLabel;
            public object CurrentValue;
        }

        private readonly ConcurrentStack<OperatorObject> _opStack = new ConcurrentStack<OperatorObject>();
        private readonly List<OaSlotObject> _oaSlot = new List<OaSlotObject>();

        public void SetupOaSlot(int slotId, string objectLabel, int parentSlot = -1)
        {
            // There are fewer slots than we need, create new ones
            // e.g. OA1 -> 0A3, but OA2 doesn't exist yet for whatever reason...
            if (slotId > _oaSlot.Count)
            {
                // For above case, endSlot would be 3, but count is 2
                // so we create 1 extra slot (OaSlot[2]) and the following
                // Insert will end up creating OaSlot[3]
                for (var i = slotId - _oaSlot.Count; i > 0; i--)
                {
                    _oaSlot.Add(null);
                }
            }

            // List<T> automatically expands for an Insert in this case
            if (slotId == _oaSlot.Count)
            {
                _oaSlot.Insert(slotId, new OaSlotObject
                                      {
                                          ParentSlot = parentSlot,
                                          ObjectLabel = objectLabel
                                      });
            }
            else
            {
                _oaSlot[slotId] = new OaSlotObject
                                 {
                                     ParentSlot = parentSlot,
                                     ObjectLabel = objectLabel
                                 };
            }
        }

        /// <summary>
        /// Adds a new operation type to the operation stack
        /// </summary>
        /// <param name="type">Type of the operation</param>
        /// <param name="arguments">Operation arguments:
        /// <para>Scan: Target slot number</para>
        /// <para>Select: Slot to operate against; Operation (= or ~ for strings, = &lt; &gt; for integers); true/false</para>
        /// <para>Aggr: </para></param>
        public void PushOperation(OperatorType type, List<object> arguments = null)
        {
            _opStack.Push(new OperatorObject { Type = type, Arguments = arguments });
        }

        /// <summary>
        /// Performs the next operation in the stack and returns each resulting value
        /// </summary>
        /// <param name="rePush">true to have the operation push back into the stack when done (internal use only)</param>
        /// <param name="op">Explicit operator to perform (operator already popped by join...internal use only)</param>
        /// <returns>Iterates through each resulting value from the operation</returns>
        public IEnumerable NextOperator(bool rePush = false, OperatorObject op = null)
        {
            if (op == null && !_opStack.TryPop(out op))
            {
                yield break;
            }

            switch (op.Type)
            {
                case OperatorType.Join:
                    foreach (var obj in Join())
                    {
                        yield return obj;
                    }
                    break;

                case OperatorType.Project:
                    var projSlot = (int)op.Arguments[0];
                    foreach (var obj in Project(projSlot))
                    {
                        yield return obj;
                    }
                    break;

                case OperatorType.Scan:
                    var scanSlot = (int)op.Arguments[0];
                    foreach (var obj in Scan(_oaSlot[scanSlot].ParentSlot, _oaSlot[scanSlot].ObjectLabel, scanSlot))
                    {
                        yield return obj;
                    }
                    break;

                case OperatorType.Select:
                    var selSlot = (int) op.Arguments[0];
                    var selOp = op.Arguments[1].ToString();
                    var selVal = op.Arguments[2];
                    foreach (var obj in Select(selSlot, selOp, selVal))
                    {
                        yield return obj;
                    }
                    break;

                case OperatorType.Aggr:
                    var aggOp = op.Arguments[0].ToString();
                    var aggStart = (int) op.Arguments[1];
                    var aggEnd = (int) op.Arguments[2];
                    foreach (var obj in Aggregate(aggOp, aggStart, aggEnd))
                    {
                        yield return obj;
                    }
                    break;
            }

            if (rePush)
            {
                _opStack.Push(op);
            }
        }

        /// <summary>
        /// Gets the OemObject or string/integer value which is in the particular OA slot
        /// </summary>
        /// <param name="slotId">OA slot ID</param>
        /// <returns>OemObject or string/integer value (depends what is being put in the slot in the query...)</returns>
        private object GetOaSlotValue(int slotId)
        {
            if (slotId < 0)
            {
                return OemTestSet.Root;
            }

            if (slotId < _oaSlot.Count)
            {
                var oso = _oaSlot[slotId];
                return oso.CurrentValue;
            }
            return null;
        }

        /// <summary>
        /// Performs one operation (left branch of query plan), 
        /// then for each resulting value, performs the next operation (right branch of query plan)
        /// </summary>
        /// <returns></returns>
        private IEnumerable Join()
        {
            var rightPopped = false;
            OperatorObject right = null;

            // Iterate through all of the left operation's values
            foreach (var o in NextOperator())
            {
                // Pop the right ourselves since NextOperator will be getting called multiple times
                if (!rightPopped)
                {
                    rightPopped = true;
                    if (!_opStack.TryPop(out right))
                    {
                        yield break;
                    }
                }

                // Iterate through all of the right operation's values, returning them up
                // to the operation which initiated this Join operation
                foreach (var obj in NextOperator(false, right))
                {
                    yield return obj;
                }
            }
        }

        /// <summary>
        /// For each resulting value of the next operation, gets the string/integer value in an OA slot
        /// </summary>
        /// <param name="slot"></param>
        /// <returns></returns>
        private IEnumerable Project(int slot)
        {
            foreach (var o in NextOperator())
            {
                var oa = GetOaSlotValue(slot);
                if (oa is OemObject)
                {
                    yield return ((OemObject) oa).Value;
                }
                else
                {
                    yield return oa;
                }
            }
        }

        /// <summary>
        /// Gets values for the object named by 'objectLabel' from the object in the 'startSlot'
        /// OA slot, and places them in the 'endSlot' OA slot
        /// </summary>
        /// <param name="startSlot"></param>
        /// <param name="objectLabel"></param>
        /// <param name="endSlot"></param>
        /// <returns></returns>
        private IEnumerable Scan(int startSlot, string objectLabel, int endSlot)
        {
            if (startSlot >= _oaSlot.Count || string.IsNullOrWhiteSpace(objectLabel))
            {
                yield break;
            }

            List<OemObject> set;
            if (startSlot < 0)
            {
                set = OemTestSet.Root;
            }
            else
            {
                // Get the OaSlotObject for the starting slot
                var startOa = GetOaSlotValue(startSlot);
                var oa = startOa as OemObject;

                // If it's values are a set, grab them
                if (oa != null && oa.Type == OemType.Set)
                {
                    set = (List<OemObject>) oa.Value;
                }
                else
                {
                    yield break;
                }
            }

            // Find all objects for the given label from the starting OA slot
            var targetObject = set.Where(o => o.Label.Equals(objectLabel)).ToList();
            if (targetObject.Count > 0)
            {
                // Begin returning each object, one at a time
                foreach (var oemObject in targetObject)
                {
                    _oaSlot[endSlot].CurrentValue = oemObject;
                    yield return oemObject;
                }
            }
        }

        /// <summary>
        /// For each resulting value of the next operation, returns only values agreeing
        /// with the given operation and comparison value
        /// </summary>
        /// <param name="startSlot">Slot to get a value from</param>
        /// <param name="op">Operation to perform (= or ~ for strings, = &lt; &gt; for integers)</param>
        /// <param name="value">String or integer to compare against</param>
        /// <returns></returns>
        private IEnumerable Select(int startSlot, string op, object value)
        {
            if (startSlot < 0)
            {
                yield break;
            }

            foreach (OemObject oemObject in NextOperator(true))
            {
                if (oemObject.Type == OemType.Integer)
                {
                    var num = (int)oemObject.Value;
                    var targetNum = int.Parse(value.ToString());

                    switch (op)
                    {
                        case "<":
                            if (num < targetNum)
                            {
                                yield return num;
                            }
                            break;

                        case ">":
                            if (num > targetNum)
                            {
                                yield return num;
                            }
                            break;

                        case "=":
                            if (num == targetNum)
                            {
                                yield return num;
                            }
                            break;
                    }
                }
                else if (oemObject.Type == OemType.String)
                {
                    var str = oemObject.Value.ToString();
                    var targetStr = value.ToString();

                    switch (op)
                    {
                        case "=":
                            if (str.Equals(targetStr))
                            {
                                yield return str;
                            }
                            break;

                        case "~":
                            if (str.Contains(targetStr))
                            {
                                yield return str;
                            }
                            break;
                    }
                }
            }
        }

        private IEnumerable Aggregate(string op, int startSlot, int endSlot)
        {
            if (op.Equals("exists"))
            {
                foreach (var obj in NextOperator(true))
                {
                    var oa = GetOaSlotValue(startSlot);
                    _oaSlot[endSlot].CurrentValue = oa != null;
                    yield return (oa != null)
                                     ? "true"
                                     : "false";
                }
            }
        }
    }
}
